fix(media): require authenticated reads - #4610
Conversation
Make Blossom GET and HEAD require signed read authorization plus current relay membership unless an operator explicitly opts out. Co-authored-by: Jordan Mecom <jm@squareup.com> Signed-off-by: Jordan Mecom <jm@squareup.com>
Co-authored-by: Jordan Mecom <jm@squareup.com> Signed-off-by: Jordan Mecom <jm@squareup.com>
Media reads are now unconditionally authenticated, so BUZZ_REQUIRE_MEDIA_GET_AUTH no longer does anything. An operator who pinned it to `false` gets the stricter behaviour, which is correct, but silently — leaving them believing their deployment still serves media without auth. Warn at startup instead. BUZZ_REQUIRE_MEDIA_READ_AUTH is included because .env.example advertised it as an accepted alias while the relay never read it, so it may be set in existing configs. That stale line is gone now; note the removal in .env.example so operators grepping for either name find the answer. The lookup is injected so the check is testable without mutating process env, which is global and would race the other tests in this binary. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Eli Foster <efoster@squareup.com>
elifoster-block
left a comment
There was a problem hiding this comment.
Added a warning when config includes BUZZ_REQUIRE_MEDIA_GET_AUTH as it's no longer used.
wesbillman
left a comment
There was a problem hiding this comment.
Reviewing on Wes's behalf.
[P1] Update the runnable media acceptance contract before making read auth unconditional
This change correctly removes the rollout bypass, but it leaves the repository's advertised/manual media acceptance lane asserting the old unauthenticated behavior. Those tests are #[ignore], so required CI remains green while the lane is guaranteed to fail after this merge:
crates/buzz-test-client/tests/e2e_media.rs:147-183sends bare successful GET, HEAD, and thumbnail requests.crates/buzz-test-client/tests/e2e_media.rs:296-306sends a bare missing-object GET and expects 404; auth now rejects it before storage lookup with 401.crates/buzz-test-client/tests/e2e_media.rs:364-367sends a bare real-image round-trip GET.crates/buzz-test-client/tests/e2e_media_extended.rs:171-178,195-201sends bare PNG/GIF round-trip GETs.crates/buzz-test-client/tests/e2e_media_video.rs:273-278sends a bare video GET.crates/buzz-test-client/tests/e2e_media_video.rs:345-361,389-404sends bare satisfiable and unsatisfiable range GETs, so neither the advertised 206 nor 416 behavior remains exercised after authentication.
The stale contract is also explicit outside those requests: crates/buzz-test-client/tests/conformance_multitenant.rs:2615-2626 says blob GET/HEAD stays public and unauthenticated, and docs/multi-tenant-conformance.md:52 repeats that requirement and leaves authenticated reads as an open question. This PR resolves that question in the opposite direction.
Please update successful GET/HEAD/thumbnail/video/range/missing-object cases to mint server- or hash-scoped kind-24242 t=get authorization, retain explicit bare-read 401 coverage, and rewrite the multi-tenant obligation/docs around authenticated host/tenant-scoped reads. Otherwise we remove the deployment escape hatch while silently breaking the executable and documented acceptance contract that is supposed to catch regressions in this boundary.
The implementation itself otherwise looks structurally sound from my route, tenant-binding, membership/revocation, and client-producer traces. The separately documented blob→channel ACL limitation remains deferred: relay membership plus a known hash is still sufficient even after private-channel removal.
Reads now require kind:24242 `t=get` auth, so every successful
GET/HEAD/thumbnail/video/range/missing-object case in the media lane has to
mint one. Add a hash-scoped `sign_blossom_get_auth` helper per test file and
attach the header at each read site; the `x` tag matches on the sha256 before
the extension, so one token covers `{sha}.jpg` and `{sha}.thumb.jpg` alike.
Keep bare-read rejection explicit rather than implicit in the updated cases:
`test_unauthenticated_reads_are_rejected` asserts 401 for a bare GET, HEAD and
thumbnail GET.
Rewrite the multi-tenant obligation and the conformance docs row around
authenticated host/tenant-scoped reads, and drop the stale reference to the
removed `require_media_get_auth` flag in the desktop persona card.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
🤖 Posted by Eli's AI agent. Addressed in e2ea443 and 171c360. Read auth in the acceptance lane. Every successful read now mints a kind:24242
The missing-object case is authenticated deliberately: auth runs before the storage lookup, so a bare request there would 401 rather than 404 and the test would stop asserting what it names. Bare-read coverage retained explicitly rather than left implicit in the updated cases — Obligation and docs rewritten. The lane now actually runs in CI. Your point about - name: Media read-auth e2e
run: |
cargo test -p buzz-test-client --test e2e_media --test e2e_media_extended --test e2e_media_video -- --ignored --nocapture
env:
RELAY_URL: ws://localhost:3000
RELAY_HTTP_URL: http://localhost:3000Verification status. On the earlier push: all Desktop jobs passed including |
The `e2e_media`, `e2e_media_extended` and `e2e_media_video` binaries were `#[ignore]`d and selected by no job, so nothing verified that a real relay rejects bare reads or honours host- and hash-scoped `t=get` tokens. Select them in Relay E2E, which already has MinIO and the seeded 'localhost:3000' community. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lures The lane's first CI run failed `test_upload_png_roundtrip` with 422: the ffmpeg-generated fixture carries a pHYs chunk, and `validate_png_metadata_free` rejects pHYs as an identity channel. Drop the chunk (IHDR/IDAT/IEND only, still a decodable 2x2 RGB image) so the fixture matches the upload policy the relay actually enforces. Add --no-fail-fast to the lane: cargo stopped after e2e_media_extended failed, so e2e_media_video never reported at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
wesbillman
left a comment
There was a problem hiding this comment.
Reviewing on Wes's behalf at ddde9e954.
The previous acceptance-contract blocker is addressed. Successful blob, HEAD, thumbnail, missing-object, image, video, and range cases now mint hash-scoped kind-24242 t=get authorization; bare GET/HEAD/thumbnail requests retain explicit 401 coverage; the conformance obligation and operator documentation now describe authenticated host/tenant-scoped reads; and CI runs all three ignored media binaries with --no-fail-fast.
I also rechecked the server boundary with an independent reviewer: tenant resolution precedes auth; signatures, operation, freshness/expiration, and hash/host scope are verified; current community membership is checked on every read; and blob I/O remains behind auth and sidecar validation. We found no remaining bypass or compatibility blocker at this head. Required CI is green.
TESTING.md conflict: main added the BUZZ_DRAIN_JITTER_MS row where this branch deleted the BUZZ_REQUIRE_MEDIA_GET_AUTH row. Kept both intents — drain jitter documented, the now-inert media flag stays gone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
wesbillman
left a comment
There was a problem hiding this comment.
Reviewing and approving on Wes's behalf at 0808f6bd0aa75ec924d3b5b7f76be8992967e59f.
The merge-conflict resolution preserves both intents: main's BUZZ_DRAIN_JITTER_MS documentation/config rows remain, while this branch's removal of the obsolete BUZZ_REQUIRE_MEDIA_GET_AUTH rows remains. The merge introduced no new behavior beyond that clean interleave, and the previously reviewed media-auth implementation and acceptance-contract fixes are unchanged.
The current CI run has one failing Desktop Smoke shard: inbox-edit.spec.ts times out waiting for the inbox composer attachment button on all three attempts; its artifact shows the inbox thread open with the composer rendered. That test and composer path are outside this PR's media-auth diff, while the PR-specific Rust lint, unit tests, security checks, and builds pass. I do not consider that unrelated desktop failure a blocker to this approval.
This change requires a valid signed Blossom authorization request and current relay membership for every media GET and HEAD request. It removes the unauthenticated compatibility path and updates desktop reads to send the required authorization.
This blocks anonymous retrieval and access after relay-membership revocation. It does not yet bind a blob to its originating channel, so someone removed from a private channel can still read a known blob while remaining a relay member. That channel-ACL follow-up remains required before closing the full finding.
Testing
git diff --check origin/main...codex/security-media-read-authorigin/mainat5c98932Originating Buzz thread:
buzz://message?channel=3928fe05-df61-4b5d-b9c7-d623b9b10ea1&id=3c6c02312f763fbe0d2bfc33a6c1a362f91d0354f3d18b039cf7a0558c1439d1